{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"images/strathsdr_banner.png\" align=\"left\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# RFSoC Radio Demonstration\n",
    "\n",
    "----\n",
    "\n",
    "<div class=\"alert alert-box alert-info\">\n",
    "Please use Jupyter Labs http://board_ip_address/lab for this notebook.\n",
    "</div>\n",
    "\n",
    "This demonstrator presents a Binary Phase Shift Keying (BPSK) and Quadrature Phase Shift Keying (QPSK) radio system design for the Zynq UltraScale+ RFSoC [1]. The BPSK/QPSK radio implements a full transmitter and receiver design including frame based transmission of BPSK and QPSK modulated data, receiver synchronisation, frame synchronisation, and payload extraction. This demonstration employs the RFSoC RF Data Converters (RF DCs) to support the transmission and reception of BPSK and QPSK waveforms. This system is based on the work described in [2].\n",
    "\n",
    "## Table of Contents\n",
    "* [1. Introduction](#introduction)\n",
    "    * [1.1. Hardware Setup](#hardware-setup)\n",
    "    * [1.2. Software Setup](#software-setup)\n",
    "* [2. The RFSoC Radio System](#rfsoc-radio)\n",
    "    * [2.1. Controlling the System](#controlling-the-system)\n",
    "* [3. The Transmitter](#the-transmitter)\n",
    "    * [3.1. Inspecting the Transmitter](#inspecting-the-transmitter)\n",
    "    * [3.2. Frame Generation](#frame-generation)\n",
    "* [4. The Receiver](#the-receiver)\n",
    "    * [4.1. Inspecting the Receiver](#inspecting-the-receiver)\n",
    "* [5. Running the Radio](#running-the-radio)\n",
    "    * [5.1. 'Hello World!' In Three Ways](#hello-world-in-three-ways)\n",
    "    * [5.2. A Repeating Message](#a-repeating-message)\n",
    "* [6. Conclusion](#conclusion)\n",
    "\n",
    "## References\n",
    "* [1] - [Xilinx, Inc, \"USP RF Data Converter: LogiCORE IP Product Guide\", PG269, v2.4, November 2020](https://www.xilinx.com/support/documentation/ip_documentation/usp_rf_data_converter/v2_4/pg269-rf-data-converter.pdf)\n",
    "* [2] - [Stewart, R. W., Barlee, K. W., Atkinson, D. S. W., & Crockett, L. H. (2015). Software Defined Radio using MATLAB & Simulink and the RTL-SDR. (1 ed.)](https://www.desktopsdr.com/)\n",
    "\n",
    "## Revision History\n",
    "* **v1.0** | 24/02/2021 | RFSoC BPSK radio demonstrator\n",
    "* **v1.1** | 16/03/2022 | Updated for RFSoC4x2\n",
    "* **v1.2** | 12/01/2023 | Updated radio to support BPSK and QPSK waveforms and added sync tests\n",
    "* **v1.3** | 15/05/2023 | Add general demonstrator notebook"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "----"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Introduction\n",
    "This notebook demonstrates a simple BPSK and QPSK transceiver design for the RFSoC. Interactive plotting capabilities are provided so that users can visualise each stage of the radio synchronisation process. Users can transmit their own messages and receive them using a terminal. Furthermore, image data can also be transmitted and received using this system. Image transmission is accompanied by simple image display widgets for visualisation. To start using the radio demonstrator, follow the hardware and software setup instructions below.\n",
    "\n",
    "### 1.1. Hardware Setup <a class=\"anchor\" id=\"hardware-setup\"></a>\n",
    "Your RFSoC development board should be setup in single channel mode with a loopback cable connected between an ADC and DAC.\n",
    "\n",
    "See the setup instructions [here](01_rfsoc_radio_setup.ipynb) for more information.\n",
    "\n",
    "<div class=\"alert alert-box alert-danger\">\n",
    "<b>Caution:</b>\n",
    "    In this demonstration, we generate tones using the RFSoC development board. Your device should be setup in loopback mode. You should understand that the RFSoC platform can also transmit RF signals wirelessly. Remember that unlicensed wireless transmission of RF signals may be illegal in your geographical location. Radio signals may also interfere with nearby devices, such as pacemakers and emergency radio equipment. Note that it is also illegal to intercept and decode particular RF signals. If you are unsure, please seek professional support.\n",
    "</div>\n",
    "\n",
    "### 1.2. Software Setup <a class=\"anchor\" id=\"software-setup\"></a>\n",
    "Lets begin by programming the FPGA bitstream and initialising the PYNQ overlay design. To do this, we need to import the `rfsoc_radio` package."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from rfsoc_radio.overlay import RadioOverlay"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we can simply initialise the overlay by downloading the bitstream and executing the drivers. Upon running the cell below, a synchronisation test will be performed to ensure your system is ready for the demonstration. If these tests fail, reset the notebook and double-check the loopback connection. Then, simply run the notebook again."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol = RadioOverlay(run_test=True, debug_test=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This demonstration has two primary software objects in the overlay design. The first is `ol.radio_transmitter`, which is the software wrapper responsible for controlling the BPSK and QPSK transmitter and corresponding Direct Memory Access (DMA) IP cores. Similarly, the second is `ol.radio_receiver`, which is another software wrapper that controls the BPSK and QPSK receiver and associated DMA. Each software wrapper can be queried by using the help function to display their doc string. Try this in the cell below for each of the software wrappers:\n",
    "\n",
    "```python\n",
    "help(ol.radio_receiver)\n",
    "help(ol.radio_transmitter)\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "help(ol.radio_receiver)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "----\n",
    "\n",
    "## 2. The RFSoC Radio System <a class=\"anchor\" id=\"rfsoc-radio\"></a>\n",
    "The aim of this notebook is to demonstrate simple BPSK and QPSK data transmission using the RFSoC and PYNQ. Two hardware accelerators are provided; one to transmit data, and the other to receive data. Each accelerator is independant of one another and do not communicate. The transmitter modulates 100 kSa/s of data and it interpolates the signal to 1.024 GSa/s with subsequent interpolation stages. The RF DAC then transmits the data. The receiver is connected to the transmitter using an SMA loopback cable. The RF ADC will initially decimate the data and the receiver hardware accelerator will be responsible for synchronising to the signal and extracting the modulated data. The entire system can be seen in the [Figure 2](fig-2).\n",
    "\n",
    "<figure> <a class=\"anchor\" id=\"fig-2\"></a>\n",
    "    <img src=\"images/system_overview.png\" style=\"width: 70%;\"/>\n",
    "    <figcaption><b>Figure 2: Overview of the radio demonstration system on RFSoC and PYNQ.</b></figcaption>\n",
    "</figure>\n",
    "\n",
    "A DMA controller is provided to transfer data from Jupyter Labs to the transmitter. Another DMA is provided to transfer data from the receiver to Jupyter Labs. Jupyter will be used to control the hardware accelerators and inspect the transmitter's data generation stages and receiver's synchronisation stages.\n",
    "\n",
    "### 2.1. Controlling the System <a class=\"anchor\" id=\"controlling-the-system\"></a>\n",
    "This demonstration provides a useful tool for interacting with the radio system. A radio dashboard, made entirely using the `ipywidgets` library, will allow you to change the mixer frequencies of the RF DAC and RF ADC. You can also choose to switch-off parts of the radio system such as the transmitter, and various parts of the receiver. You can load the radio dashboard by running the code cell below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.dashboard()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "* _**After you have executed the above cell, you should right click the radio dashboard, and select \"Create New View for Output\" from the drop-down menu. This will allow you to interact with the remainder of the notebook and retain access to the radio dashboard in another output view.**_\n",
    "\n",
    "The radio dashboard also reports the frequency offset that is corrected in the coarse frequency synchronisation stage. Be aware that the coarse frequency synchroniser can only correct 1.6MHz offsets. If the signal's offset frequency is larger than 1.6MHz, the reported frequency offset will be incorrect.\n",
    "\n",
    "## 3. The Transmitter <a class=\"anchor\" id=\"the-transmitter\"></a>\n",
    "The transmitter modulates 8-bit wide fixed point data into BPSK or QPSK packets for transmission. Initially, the 8-bit wide data is serialised and modulated. The transmission system uses a root raised cosine (RRC) to pulse-shape the modulated data, and then interpolates the samples to 1.024 GSa/s. The pulse-shaped, interpolated data is mixed with a carrier frequency and transmitted using the RF DAC. An overview of the transmitter can be seen in [Figure 3](fig-3).\n",
    "\n",
    "<figure> <a class=\"anchor\" id=\"fig-3\"></a>\n",
    "    <img src=\"images/transmitter.png\" style=\"width: 90%;\"/>\n",
    "    <figcaption><b>Figure 3: System architecture of the transmitter.</b></figcaption>\n",
    "</figure>\n",
    "\n",
    "### 3.1. Inspecting the Transmitter <a class=\"anchor\" id=\"inspecting-the-transmitter\"></a>\n",
    "When the transmitter hardware system was designed, a data inspector was added that will allow you to transfer frames of data from the PL to the external memory. Software has been developed that manipulates the samples of data for visualisation using the Python Plotly library. You can try this for yourself by running the code cell below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.radio_transmitter.visualise()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Upon executing the cell above you will be presented with time domain, constellation, and frequency spectrum plots. These plots can be continuously updated by clicking the start button. You are able to visualise other points in the transmitter by using the observation point dropdown menu.\n",
    "\n",
    "Notice in [Figure 3](fig-3) that a DMA controller is provided to transfer data from Jupyter Labs to the transmitter. We can interact with the DMA through the `ol.radio_transmitter` software wrapper by calling two methods.\n",
    "\n",
    "```python\n",
    "ol.radio_transmitter.data(message)\n",
    "ol.radio_transmitter.start()\n",
    "```\n",
    "\n",
    "The first method loads an ascii message into the transmitter's buffer. The second method will start the transmission. If you run these methods right now, you won't be able to see very much as we haven't prepared our receiver yet. Furthermore, the receiver will need support when trying to detect our transmitted signal. This support can be provided through frame based data transmission."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.2. Frame Generation <a class=\"anchor\" id=\"frame-generation\"></a>\n",
    "Before we continue to explore the receiver, the data frame created by the transmitter software wrapper should be described. Data frames are often required in communication systems as they allow the receiver to synchronise to the start of the payload (the data we would like to extract). Usually the transmitter sends a known sequence of bits, and these bits are detected in the receiver through correlation. In this system, the extended barker sequence [2] is used as the known sequence of bits for frame synchronisation.\n",
    "\n",
    "In addition to the barker sequence, the transmitter must place other important information in a data frame before transmission. Some of these are listed below:\n",
    "\n",
    "* Random data that allows the synchroniser to be exercised.\n",
    "* The extended barker sequence or known sequence of bits.\n",
    "* The frame number (if there are a series of frames transmitted sequentially).\n",
    "* The start flag indicating the first frame in the sequence.\n",
    "* The end flag indicating the last frame in the sequence.\n",
    "* The data frame length.\n",
    "* The payload.\n",
    "* Zero padding.\n",
    "* Anything else that is useful for your application.\n",
    "\n",
    "The radio demonstration system uses a custom data frame. To keep things simple, the transmitter software wrapper only sends data frames that are 64 bytes long. This means that the frame is aligned to 4 bytes in memory, preventing unaligned access. The data frame format used by the demonstrator is shown in [Figure 4](fig-4).\n",
    "\n",
    "<figure> <a class=\"anchor\" id=\"fig-4\"></a>\n",
    "    <img src=\"images/data_frame.png\" style=\"width: 80%;\"/>\n",
    "    <figcaption><b>Figure 4: Data frame structure for transmit and receive packets.</b></figcaption>\n",
    "</figure>\n",
    "\n",
    "As shown, the data frame is exactly 64 bytes long. If the data frame uses less than 44 bytes of data for the payload, it is zero padded as required. Each of the data frame entries shown are used in this demonstration system.\n",
    "\n",
    "Lets now investigate the receiver and prepare it for receiving waveforms."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. The Receiver <a class=\"anchor\" id=\"the-receiver\"></a>\n",
    "The receiver consists of five stages that help it acquire a BPSK or QPSK waveform. These stages are:\n",
    "* Decimation\n",
    "* Coarse Frequency Synchronisation\n",
    "* Matched Filtering\n",
    "* Time and Phase Synchronisation\n",
    "* Frame Synchronisation\n",
    "\n",
    "You can see each of these stages below in [Figure 5](fig-5). Notice that the final stage, frame synchronisation, is directly connected to a DMA controller. This allows the RFSoC's PL to transfer demodulated frames into Jupyter Labs, via external memory.\n",
    "\n",
    "<figure> <a class=\"anchor\" id=\"fig-5\"></a>\n",
    "    <img src=\"images/receiver.png\" style=\"width: 90%;\"/>\n",
    "    <figcaption><b>Figure 5: System architecture of the receiver.</b></figcaption>\n",
    "</figure>\n",
    "\n",
    "The decimation stage significantly decreases the sample rate of the signal to 12.8MSa/s. Coarse frequency synchronisation is then performed correcting frequency offsets up to 1.6MHz. These frequency offsets must be larger than 195.3125Hz in order for the coarse frequency synchroniser to correct the signal. A matched root rasied cosine filter is then applied to the signal to suppress intersymbol interference. Time and phase synchronisation obtains the maximum effect points of the signal and decides whether the received signal is a binary 1 or 0. Finally, frame synchronisation is performed by correlating an extended barker sequence with the received binary data. This detects the start of the frame, which is transferred into system memory by the DMA.\n",
    "\n",
    "### 4.1. Inspecting the Receiver <a class=\"anchor\" id=\"inspecting-the-receiver\"></a>\n",
    "When the receiver hardware system was designed, a data inspector was added that will allow you to transfer frames of data from the PL to the external Processing System (PS) memory. Software has been developed that manipulates the samples of data for visualisation using the Python Plotly library. You can try this for yourself by running the code cell below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.radio_receiver.visualise()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Upon executing the cell you will be presented with time domain, constellation, and frequency spectrum plots. These plots can be continuously updated by clicking the start button. You are able to visualise other points in the receiver by using the observation point dropdown menu. You should see maxium effect points extracted in the phase synchronisation observation point. If you see a noisy signal, try resetting the phase and time synchronisation blocks using the dashboard.\n",
    "\n",
    "* _**Right-click the area above, and in the drop-down menu that appears, select \"Create New View for Output\". This action will move the plots to another window in Jupyter Labs, allowing you to scroll further down the notebook while still being able to visualise and interact with the plots.**_\n",
    "\n",
    "----\n",
    "\n",
    "## 5. Running the Radio <a class=\"anchor\" id=\"running-the-radio\"></a>\n",
    "This RFSoC demonstration system has been designed to simplify data movement between the RFSoC's PS and PL. It is worth noting here that the transmitter and receiver systems are entirely independant of one another. That is, they do not share common clocks and there are no hidden loopbacks in the logic fabric. To transmit and receive using the radio demonstrator, we are relying entirely on the RF DC interface.\n",
    "\n",
    "Ascii terminals have been created using `ipywidgets` that allow you to visualise and interact with transmitted and received data. Lets start by creating the receiver terminal by running the code cell below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.radio_receiver.terminal()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The receiver terminal will appear. You won't be able to interact with the main text box as this terminal is for receiving messages only. That means we need to transmit a message first before it will appear in the box above. You can configure the receiver terminal using the buttons on the right. The functionality of each button is as follows:\n",
    "\n",
    "* **Play button** - Listen for transmitted BPSK and QPSK waveforms with the extended barker sequence and print them in the terminal.\n",
    "* **Stop button** - Do not listen for transmitted BPSK and QPSK waveforms with the extended barker sequence.\n",
    "* **Clear button** - Clear the terminal.\n",
    "* **Auto Clear button** - Automatically clear the terminal after 10 messages have been received.\n",
    "* **Debug button** - When enabled, inspect the frame's meta data and payload information. When disabled, only show the payload.\n",
    "\n",
    "Lets now put our receiver terminal to good use by transmitting a QPSK signal with 'Hello World!'.\n",
    "\n",
    "* _**Right-click the receiver terminal above, and in the drop-down menu that appears, select \"Create New View for Output\". This action will move the terminal to another window in Jupyter Labs, allowing you to scroll further down the notebook while still being able to visualise and interact with the plots.**_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.1. 'Hello World!' In Three Ways <a class=\"anchor\" id=\"hello-world-in-three-ways\"></a>\n",
    "Lets dive straight in and perform a 'Hello World!'. Run the code cell below to initiate a transfer between RF DAC and RF ADC."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.radio_transmitter.data('Hello World!\\r')\n",
    "ol.radio_transmitter.start()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If everything worked out okay, you should see a 'Hello World!' message in the receiver terminal. If not, follow some of the suggestions below:\n",
    "\n",
    "* Using the radio dashboard, reset each of the receiver subsystems until phase synchronisation is achieved.\n",
    "* Ensure that the play button on the receiver terminal is enabled (green colour).\n",
    "* Ensure that the loopback on your development board is connected correctly.\n",
    "* Using the radio dashboard, ensure that the centre frequencies for the RF ADC and RF DAC are the same.\n",
    "\n",
    "Now that you have transmitted your first hello world, lets do another transmission but this time ensure that the **Debug Button** on the receiver terminal is enabled (blue colour). You can see this button enabled in [Figure 6](fig-6) below. This will allow you to inspect the frame information when you run the next code cell.\n",
    "\n",
    "<figure> <a class=\"anchor\" id=\"fig-6\"></a>\n",
    "    <img src=\"images/receiver_terminal_debug.jpg\" style=\"width: 30%;\"/>\n",
    "    <figcaption><b>Figure 6: Switch on the Debug button.</b></figcaption>\n",
    "</figure>\n",
    "\n",
    "Now run the next code cell, which will transmit several QPSK frames rather than just one."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.radio_transmitter.data(''.join(['The quick brown fox jumps over the lazy dog.\\r',\n",
    "                             'How razorback-jumping frogs can level six piqued gymnasts.\\r']))\n",
    "ol.radio_transmitter.start()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the receiver terminal you will now be able to see verbose information about the packets that were just sent. A total of 3 packets will have been received. You can use this debug feature to inspect the frame meta data.\n",
    "\n",
    "* _**From this point on you should switch off the debug feature using the debug button and clear your receiver terminal using the clear button.**_\n",
    "\n",
    "Lastly, we will show another way to transmit ascii data using the transmitter. Another terminal can be created that will allow you to insert ascii data directly into the transmitter. Running the code cell below, will create a transmitter terminal."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.radio_transmitter.terminal()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To use this terminal, all you have to do is insert a message and press the send button. After you press the send button, the terminal will automatically clear ready for the next input. Try this now and use the receiver terminal to inspect the received data.\n",
    "\n",
    "At this point, it is worth mentioning that you are also able to change the modulation scheme from QPSK to BPSK at any time using the radio dashboard. Try changing the modulation scheme now from QPSK to BPSK and inspect the constellation diagram of the transmitter (symbol generation) or receiver (time synchronisation). The modulation scheme should change appropriately and be easy to see in the given plot."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.2. A Repeating Message <a class=\"anchor\" id=\"a-repeating-message\"></a>\n",
    "The last part of the demonstration is a transmit repeat message. Up until now, the entire system has been controlled by software. You did not need to design any hardware to transmit a variety of different messages and perform receiver introspection. We will stick with this theme and introduce a simple timer thread class, which will allow you to execute a function at a specified rate and number of iterations. Run the code cell below to create the timer thread class."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import threading\n",
    "import time\n",
    "\n",
    "class TimerThread():\n",
    "    def __init__(self,\n",
    "                 callback,\n",
    "                 rate=0.5,\n",
    "                 iterations=20):\n",
    "    \n",
    "        self.callback = callback\n",
    "        self.rate = rate\n",
    "        self.iterations = iterations\n",
    "        self.stopped = True\n",
    "    \n",
    "    def start(self):\n",
    "        if self.stopped:\n",
    "            thread = threading.Thread(target=self._do)\n",
    "            thread.start()\n",
    "            \n",
    "    def _do(self):\n",
    "        self.stopped = False\n",
    "        iterations = 0\n",
    "        while iterations < self.iterations:\n",
    "            next_timer = time.time() + self.rate\n",
    "            self.callback()\n",
    "            iterations += 1\n",
    "            sleep_time = next_timer - time.time()\n",
    "            if sleep_time > 0:\n",
    "                time.sleep(sleep_time)\n",
    "            if self.stopped:\n",
    "                break\n",
    "        self.stopped = True\n",
    "        \n",
    "    def stop(self):\n",
    "        self.stopped = True"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The timer thread class accepts three arguments, a callback, the rate of the callback function, and the number of times the callback should be executed. We can simply create a function that can be used as the repeating message callback as below in `transmitter_callback()`. This function uses a global counter, to create a 'Hello World!' message with a number appended at the end. The callback will be passed to the timer thread class for execution.\n",
    "\n",
    "Run the code cell below to create the TimerThread object and transmitter callback."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "counter = 0\n",
    "\n",
    "def transmitter_callback():\n",
    "    global counter\n",
    "    message = ''.join(['Hello World! ', str(counter), '\\r'])\n",
    "    ol.radio_transmitter.data(message)\n",
    "    ol.radio_transmitter.start()\n",
    "    counter += 1\n",
    "\n",
    "tx_repeater = TimerThread(callback=transmitter_callback,\n",
    "                          rate=0.5,\n",
    "                          iterations=20)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can now start the thread by running the code cell below. Doing so will create a new thread that will execute the `transmitter_callback()` function 20 times, every 0.5 seconds. After the number of iterations have been achieved, the thread will exit. Before running the code cell below, ensure you can see your receiver terminal."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tx_repeater.start()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If at any point you wish to stop the thread, simply use the cell below. Remember the thread will stop anyway when the number of iterations have been achieved."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tx_repeater.stop()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6. Conclusion <a class=\"anchor\" id=\"conclusion\"></a>\n",
    "This notebook has presented a simple BPSK and QPSK radio demonstrator on the RFSoC using PYNQ. It was shown that data can be transmitted and received correctly using BPSK and QPSK modulation and the RF DCs.\n",
    "\n",
    "----\n",
    "\n",
    "⬅️ [Previous Notebook](01_rfsoc_radio_setup.ipynb)\n",
    "\n",
    "----\n",
    "----"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
